package be.sc.classicnetwork.dao.hibernate;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import javax.persistence.OneToMany;

import org.apache.log4j.Logger;
import org.hibernate.FetchMode;
import org.hibernate.criterion.Criterion;
import org.hibernate.criterion.DetachedCriteria;
import org.hibernate.criterion.Order;
import org.hibernate.criterion.Projections;
import org.hibernate.criterion.Restrictions;
import org.springframework.dao.support.DataAccessUtils;
import org.springframework.orm.hibernate3.support.HibernateDaoSupport;
import org.springframework.util.Assert;

import be.sc.classicnetwork.dao.GenericDao;
import be.sc.classicnetwork.exception.ApplicationException;
import be.sc.classicnetwork.model.AbstractEntity;

/**
 * Implementation of the Generic DAO interface using Hibernate OR mapper with
 * CRUD functionalities and contains some helpful methods for DAOs inheriting
 * from this class.
 */
public class HibernateGenericDao<T extends AbstractEntity> extends
		HibernateDaoSupport implements GenericDao<T> {

	/** the logger */
	protected final Logger log = Logger.getLogger(this.getClass());

	private Class<T> clazz;

	/**
	 * Constructor.
	 */
	public HibernateGenericDao() {
		super();
	}

	/**
	 * Constructor.
	 */
	public HibernateGenericDao(Class<T> clazz) {
		super();
		this.clazz = clazz;
	}

	/**
	 * @see be.sc.classicnetwork.dao.GenericDao#findById(int)
	 */
	@SuppressWarnings("unchecked")
	public T findById(String id) {
		log.debug("Finding " + clazz.getName() + " with id = " + id);
		return (T) getHibernateTemplate().get(clazz, id);
	}

	/**
	 * Gets the field names having an Annotation <code>@OneToMany</code> or <code>@ManyToMany</code>.
	 */
	private String[] getOneToManyFields(Class<? extends AbstractEntity> clazz) {
		// check param
		Assert.notNull(clazz);

		// get all fields of the class
		Field[] allFields;
		try {
			allFields = clazz.getDeclaredFields();
		} catch (SecurityException e) {
			throw new ApplicationException(e);
		}

		// get the fields having a @OneToMany
		ArrayList<String> oneToManyAnnotatedFields = new ArrayList<String>(
				allFields.length);
		for (Field f : allFields) {
			if (f.getAnnotation(OneToMany.class) != null) {
				oneToManyAnnotatedFields.add(f.getName());
			}
		}

		return oneToManyAnnotatedFields.toArray(new String[0]);
	}

	/**
	 * @see be.sc.classicnetwork.dao.GenericDao#findByIdFull(int)
	 */
	@SuppressWarnings("unchecked")
	public T findByIdFull(String id) {
		log.debug("Finding full " + clazz.getName() + " with id = " + id);

		// get the fields having a @OneToMany and fetch them
		String[] oneToManyFields = getOneToManyFields(clazz);
		DetachedCriteria criteria = DetachedCriteria.forClass(clazz);
		for (String f : oneToManyFields) {
			criteria.setFetchMode(f, FetchMode.JOIN);
		}

		// add id criterion
		criteria.add(Restrictions.idEq(id));

		return (T) DataAccessUtils.uniqueResult(getHibernateTemplate()
				.findByCriteria(criteria));
	}

	/**
	 * @see be.sc.classicnetwork.dao.GenericDao#findAll(int, int)
	 */
	@SuppressWarnings("unchecked")
	public List<T> findAll(int first, int max) {
		log.debug("Finding all " + clazz.getName());
		return findByCriteria(first, max, null, new Criterion[0], null);
	}

	/**
	 * @see be.sc.classicnetwork.dao.GenericDao#saveOrUpdate(be.sc.classicnetwork.model.AbstractEntity)
	 */
	public T saveOrUpdate(T entity) {
		log.debug("Saving " + clazz.getName() + " with id = " + entity.getId());
		getHibernateTemplate().saveOrUpdate(entity);
		return entity;
	}

	/**
	 * @see be.sc.classicnetwork.dao.GenericDao#delete(be.sc.classicnetwork.model.AbstractEntity)
	 */
	public void delete(T entity) {
		log.debug("Deleting " + clazz.getName() + " with id = "
				+ entity.getId());
		getHibernateTemplate().delete(entity);
	}

	/**
	 * @see be.sc.classicnetwork.dao.GenericDao#deleteAll()
	 */
	public void deleteAll() {
		log.debug("Deleting all " + clazz.getName());
		getHibernateTemplate().deleteAll(findAll(0, 0));
	}

	/**
	 * @see be.sc.classicnetwork.dao.GenericDao#countAll()
	 */
	public int countAll() {
		log.debug("Counting all " + clazz.getName());
		return countByCriteria(null, new Criterion[0]);
	}

	/**
	 * @see be.sc.classicnetwork.dao.GenericDao#findByIdIn(java.lang.Integer[])
	 */
	public List<T> findByIdIn(String... ids) {
		return findByCriteria(0, 0, null, new Criterion[] { Restrictions.in(
				"id", ids) }, null);
	}

	/**
	 * Gets a DetachedCriteria from a set of Criterion.
	 * 
	 * @param criteria
	 * @return
	 */
	private DetachedCriteria getCriteria(Map<String, String> aliases,
			Criterion[] criteria, Order[] orders) {
		DetachedCriteria detachedCriteria = DetachedCriteria.forClass(clazz);

		// set the aliases if any
		if (aliases != null && !aliases.isEmpty()) {
			for (Map.Entry<String, String> alias : aliases.entrySet()) {
				detachedCriteria.createAlias(alias.getKey(), alias.getValue());
			}
		}

		// set criteria
		if (criteria != null && criteria.length > 0) {
			for (Criterion c : criteria) {
				detachedCriteria.add(c);
			}
		}

		// set the order by clause(s) if any
		if (orders != null && orders.length > 0) {
			for (Order order : orders) {
				detachedCriteria.addOrder(order);
			}
		}

		return detachedCriteria;
	}

	/**
	 * findByCriteria.
	 */
	@SuppressWarnings("unchecked")
	protected List<T> findByCriteria(int first, int max,
			Map<String, String> aliases, Criterion[] criteria, Order[] orders) {
		log.debug("Finding " + clazz.getName() + " by criteria");
		return getHibernateTemplate().findByCriteria(
				getCriteria(aliases, criteria, orders), first, max);
	}

	/**
	 * Counts the entities matching the specified criteria.
	 * 
	 * @param criteria
	 * @return
	 */
	protected int countByCriteria(Map<String, String> aliases,
			Criterion... criteria) {
		log.debug("Counting " + clazz.getName() + " by criteria");
		DetachedCriteria detachedCriteria = getCriteria(aliases, criteria, null);

		// set count projection
		detachedCriteria.setProjection(Projections.rowCount());

		return DataAccessUtils.intResult(getHibernateTemplate().findByCriteria(
				detachedCriteria));
	}

	public Class<T> getClazz() {
		return clazz;
	}

	public void setClazz(Class<T> clazz) {
		this.clazz = clazz;
	}
}
